package net.chromedpork.hypertext.client;



import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import java.net.HttpURLConnection;
import java.net.URL;

import java.util.Iterator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import net.chromedpork.hypertext.validation.Assertion;



/**
 *
 *  HTTP{S} client object.
 * 
 *  @author Inode <inode@chromedpork.net>
 *
 */

public class Client
{
    private HttpURLConnection    connection;

    private Map<String, String>  responseHeaders;
    private String               responsePayload;



    /**
     *
     *  Emptiness is godliness, and god is empty, just like this constructor.
     *
     */
    
    Client()
    {
    }



    /**
     *
     *  Constructs a client object for requesting URL with the GET method.
     *
     *  @param url      uniform resource locator
     *
     *  @throws ClientException
     *
     */

    Client(String url) throws ClientException
    {
        request(url);
    }    



    /**
     *
     *  Consturcts a client object for requesting URL with the prescribed
     *  method.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *
     *  @throws ClientException
     *
     */

    Client(String method, String url) throws ClientException
    {
        request(method, url);
    }



    /**
     *
     *  Constructs a client object for requesting URL with the prescribed
     *  method and passed payload data.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *  @param payload  payload data (usually only applicable when using POST)
     *
     *  @throws ClientException
     *
     */

    Client(String method,
           String url,
           String payload) throws ClientException
    {
        request(method, url, payload);
    }



    /**
     *
     *  Constructs a client object for requesting URL with the prescribed
     *  method and passed request headers.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *  @param headers  header key/value map
     *
     *  @throws ClientException
     *
     */

    Client(String method,
           String url,
           Map<String, String> headers) throws ClientException
    {
        request(method, url, headers);
    }



    /**
     *
     *  Constructs a client object for requesting URL with the prescribed
     *  method, request headers and passed payload data.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *  @param headers  header key/value map
     *  @param payload  payload data (usually only applicable when using POST)
     *
     *  @throws ClientException
     *
     */

    Client(String method,
           String url,
           Map<String, String> headers,
           String payload) throws ClientException
    {
        request(method, url, headers, payload);
    }


    
    /**
     *
     *  Generates an HTTP request, using the GET method, for the passed URL.
     *
     *  @param url      uniform resource locator
     *
     *  @throws ClientException
     *
     */
    
    public void request(String url) throws ClientException
    {
        request("GET", url, null, null);
    }



    /**
     *
     *  Generates an HTTP request, using the prescribed method, for the
     *  passed URL.
     *
     *  @param url      uniform resource locator
     *
     *  @throws ClientException
     * 
     */

    public void request(String method,
                        String url) throws ClientException
    {
        request(method, url, null, null);
    }



    /**
     *
     *  Generates an HTTP request, using the prescribed method and payload
     *  data, for the passed URL.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *
     *  @throws ClientException
     * 
     */

    public void request(String method,
                        String url,
                        String payload) throws ClientException
    {
        request(method, url, null, payload);
    }



    /**
     *
     *  Generates an HTTP request, using the prescribed method and headers,
     *  for the passed URL.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *  @param headers  header key/value map
     *
     *  @throws ClientException
     *
     */

    public void request(String method,
                        String url,
                        Map<String, String> headers) throws ClientException
    {
        request(method, url, headers, null);
    }



    /**
     *
     *  Generates an HTTP request, using the prescribed method, headers and
     *  payload data, for the passed URL.
     *
     *  @param method   request method
     *  @param url      uniform resource locator
     *  @param headers  header key/value map
     *  @param payload  payload data (usually only applicable when using POST)
     *
     *  @throws ClientException
     *
     */

    public void request(String method,
                        String url,
                        Map<String, String> headers,
                        String payload) throws ClientException
    {
        // Ensure the requisite argument sanity.
        Assertion.notNull(url, "request url is null");

        
        // Clear stale response headers and payload.
        responseHeaders = null;
        responsePayload = null;


        try
        {
            openConnection(url);

            if (method == null)
            {
                method = "GET";
            }

            connection.setRequestMethod(method);
        }

        catch (Exception exception)
        {
            throw new ClientException(exception.getMessage());
        }


        // If headers were passed, assign them to the request.
        if (headers != null)
        {
            setRequestHeaders(headers);
        }

        // The same applies if a payload was passed.
        if ((payload != null) && (payload.matches("") == false))
        {
            // Persuade the connector to spawn an output stream.
            connection.setDoOutput(true);
            setRequestPayload(payload);
        }
    }   



    /**
     *
     *  Gather the response code of the last requst.
     *
     *  @return response code
     *
     *  @throws ClientException
     *
     */

    public int getResponseCode() throws ClientException
    {
        try
        {
            return (connection.getResponseCode());
        }

        catch (IOException exception)
        {
            throw new ClientException(exception.getMessage());
        }
    }



    /**
     *
     *  Gather the response message of the last requst.
     *
     *  @return response message
     *
     *  @throws ClientException
     *
     */
    
    public String getResponseMessage() throws ClientException
    {
        try
        {
            return (connection.getResponseMessage());
        }

        catch (IOException exception)
        {
            throw new ClientException(exception.getMessage());
        }
    }



    /**
     *
     *  Gather the response status (code + message) of the last requst.
     *
     *  @return response status
     *
     *  @throws ClientException
     *
     */
    
    public String getResponseStatus()
    {
        return (connection.getHeaderFields().get(null).get(0));
    }



    /**
     *
     *  Gather the response headers associated with the last requst.
     *
     *  @return response headers
     *
     *  @throws ClientException
     *
     */
    
    public Map<String, String> getResponseHeaders()
    {
        if (responseHeaders == null)
        {
            responseHeaders = gatherResponseHeaders();
        }

        return (responseHeaders);
    }



    /**
     *
     *  Gather the response payload of the last requst.
     *
     *  @return response payload
     *
     *  @throws ClientException
     *
     */
    
    public String getResponsePayload() throws ClientException
    {
        if (responsePayload == null)
        {
            responsePayload = gatherResponsePayload();
        }

        return (responsePayload);
    }



    /**
     *
     *  Opens a connection to the HTTP server implied by the URL.
     *
     *  @throws ClientException
     *
     */

    private void openConnection(String url) throws ClientException
    {
        try
        {
            URL urlObject = new URL(url);
            connection    = (HttpURLConnection)urlObject.openConnection();
        }

        catch (Exception exception)
        {
            throw new ClientException(exception.getMessage());
        }
    }

    

    /**
     *
     *  Gathers the headers from the response to the last request generated.
     *
     *  @return response header KV map
     *
     */

    private Map<String, String> gatherResponseHeaders()
    {
        Map<String, List<String>> headers  = connection.getHeaderFields();
        Set<String>               keySet   = headers.keySet();
        Iterator                  iterator = keySet.iterator();

        Map<String, String>       result   = new HashMap<String, String>();


        while (iterator.hasNext())
        {
            String headerKey = (String)iterator.next();

            if (headerKey != null)
            {
                result.put(headerKey, headers.get(headerKey).get(0));
            }
        }

        return (result);
    }
    


    /**
     *
     *  Gathers the payload from the response to the last request generated.
     *
     *  @return response header KV map
     *
     *  @throws ClientException
     *
     */

    private String gatherResponsePayload() throws ClientException
    {
        try
        {
            DataInputStream in = new DataInputStream(connection.getInputStream());

            StringBuffer buffer = new StringBuffer();

            for (int ch = in.read(); ch != -1; ch = in.read())
            {
                buffer.append((char)ch);
            }

            in.close();

            return (buffer.toString());
        }

        catch (IOException exception)
        {
            throw new ClientException(exception.getMessage());
        }
    }

    

    /**
     *
     *  Assign request headers to the encapsulated HTTP client.
     *
     */

    private void setRequestHeaders(Map<String, String> headers)
    {
        Iterator headerIterator = headers.keySet().iterator();

        while (headerIterator.hasNext())
        {
            String headerKey = (String)headerIterator.next();

            if (headerKey != null)
            {
                connection.setRequestProperty(headerKey,
                                              headers.get(headerKey));
            }
        }
    }



    /**
     *
     *  Writes request payload data to an output stream associated with the
     *  encapsulated HTTP client.
     * 
     *  @param payload      payload/data string
     *
     *  @throws ClientException
     *
     */

    private void setRequestPayload(String payload) throws ClientException
    {
        try
        {
            DataOutputStream out = new DataOutputStream(connection.getOutputStream());

            out.writeBytes(payload);
            out.close();
        }

        catch (IOException exception)
        {
            throw new ClientException(exception.getMessage());
        }
    }
}
